﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;

namespace CodePlex.Resourceful.Microsoft.Azure
{


   

    public class AzureTableClient : BaseAzureClient
    {


        public static readonly string Version = "2009-04-14";

        public class BatchableRequest
        {
     
            private readonly string mMethod;
            private readonly string mPath;
            private readonly NameValuePairs mPathParameters;
            private readonly NameValuePairs mHeaders;
            private readonly Stream mInputStream;

            public BatchableRequest(string method, string path,NameValuePairs pathParameters, NameValuePairs headers, Stream inputStream)
            {
                mMethod = method;
                mPath = path;
                mPathParameters = pathParameters;
                mHeaders = headers;
                mInputStream = inputStream;
            }

            public string Method { get { return mMethod; } }
            public string Path { get { return mPath; } }
            public NameValuePairs PathParameters { get { return mPathParameters; } }
            public NameValuePairs Headers { get { return mHeaders; } }
            public Stream InputStream { get { return mInputStream; } }

            

        }




         public AzureTableClient(string accountName, string base64key)
            : this(accountName, base64key, true)
        {
        }

        public AzureTableClient(string accountName, string base64key,bool useSSL)
            : this(accountName, base64key, AzureConstants.PublicTableServiceHost, false, useSSL)
        {
        }

        public AzureTableClient(string accountName, string base64key, string serviceHost, bool usePathStyle, bool useSSL)
            : base(accountName, base64key, serviceHost, usePathStyle, useSSL, Version)
         {
            
         }



        public ClientResponse QueryTables()
        {
            return QueryTables(null);
        }
        public ClientResponse QueryTables(string nextTableName)
        {
            //  GET  http://myaccount.table.core.windows.net/Tables

            NameValuePairs pathParameters = new NameValuePairs();
            if (pathParameters != null)
                pathParameters["NextTableName"] = nextTableName;
           
            return DoAzureRequest(HttpMethod.Get, "Tables", null, pathParameters, null);
        }


        public ClientResponse CreateTable(string tableName)
        {
            // POST http://myaccount.table.core.windows.net/Tables


            // table names:
            //*  Table names may contain only alphanumeric characters.
            //* A table name may not begin with a numeric character.
            //* Table names are case-insensitive.
            //* Table names must be from 3 through 63 characters long. 

            // The request body is an ADO.NET entity set, which is an Atom feed. It has the following general format. 

//<?xml version="1.0" encoding="utf-8" standalone="yes"?>   
//  <entry xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" 
//    xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata
//    xmlns="http://www.w3.org/2005/Atom"> 
//    <title /> 
//    <updated>2009-03-18T11:48:34.9840639-07:00</updated> 
//    <author>
//      <name/> 
//    </author> 
//    <id/> 
//    <content type="application/xml">
//      <m:properties>
//        <d:TableName>mytable</d:TableName>
//      </m:properties>
//    </content> 
//  </entry>


            // The response body is also an ADO.NET entity set.

//<?xml version="1.0" encoding="utf-8" standalone="yes"?>
//<entry xml:base="http://myaccount.table.core.windows.net/" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom"> <id>http://myaccount.table.core.windows.net/Tables('mytable')</id>
//  <title type="text"></title>
//  <updated>2009-01-04T17:18:54.7062347Z</updated>
//  <author>
//    <name />
//  </author>
//  <link rel="edit" title="Tables" href="Tables('mytable')" />
//  <category term="myaccount.Tables" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
//  <content type="application/xml">
//    <m:properties>
//      <d:TableName>mytable</d:TableName>
//    </m:properties>
//  </content>
//</entry>

           
//            string body = string.Format(
//@"<?xml version=""1.0"" encoding=""utf-8"" standalone=""yes""?>   
//  <entry xmlns:d=""http://schemas.microsoft.com/ado/2007/08/dataservices"" 
//    xmlns:m=""http://schemas.microsoft.com/ado/2007/08/dataservices/metadata""
//    xmlns=""http://www.w3.org/2005/Atom""> 
//    <content type=""application/xml"">
//      <m:properties>
//        <d:TableName>{0}</d:TableName>
//      </m:properties>
//    </content> 
//  </entry>", tableName);


            

            NameValuePairs headers = new NameValuePairs();
            headers[HttpHeader.ContentType] = MediaType.ApplicationAtomXml;

            Stream inputStream = GetXml(AzureProperty.String("TableName", tableName));

            return this.DoAzureRequest(HttpMethod.Post, "Tables", null, null, headers, true, inputStream,null);

        }

       

        public ClientResponse DeleteTable(string tableName)
        {
            // DELETE http://myaccount.table.core.windows.net/Tables('mytable')


            NameValuePairs headers = new NameValuePairs();
            headers[HttpHeader.ContentType] = MediaType.ApplicationAtomXml;

            return this.DoAzureRequest(HttpMethod.Delete, string.Format("Tables('{0}')", tableName), null, null, headers);

        }













        public ClientResponse InsertEntity(string tableName, string partitionKey, string rowKey, params AzureProperty[] properties)
        {
            return this.Execute(this.InsertEntityRequest(tableName, partitionKey, rowKey, properties));
        }
        public BatchableRequest InsertEntityRequest(string tableName, string partitionKey, string rowKey, params AzureProperty[] properties)
        {
            //  POST http://myaccount.table.core.windows.net/mytable

// REQUEST
//<?xml version="1.0" encoding="utf-8" standalone="yes"?>
//<entry xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
//  <title />
//  <updated>2008-09-18T23:46:19.3857256Z<updated/>
//  <author>
//    <name />
//  </author>
//  <id />
//  <content type="application/xml">
//    <m:properties>
//      <d:Address>Mountain View</d:Address>
//      <d:Age m:type="Edm.Int32">23</d:Age>
//      <d:AmountDue m:type="Edm.Double">200.23</d:AmountDue>
//      <d:BinaryData m:type="Edm.Binary" m:null="true" />
//      <d:CustomerCode m:type="Edm.Guid">c9da6455-213d-42c9-9a79-3e9149a57833</d:CustomerCode>
//      <d:CustomerSince m:type="Edm.DateTime">2008-07-10T00:00:00</d:CustomerSince>
//      <d:IsActive m:type="Edm.Boolean">true</d:IsActive>
//      <d:NumOfOrders m:type="Edm.Int64">255</d:NumOfOrders>
//      <d:PartitionKey>mypartitionkey</d:PartitionKey>
//      <d:RowKey>myrowkey1</d:RowKey>
//      <d:Timestamp m:type="Edm.DateTime">0001-01-01T00:00:00</d:Timestamp>
//    </m:properties>
//  </content>
//</entry>


// RESPONSE
//<?xml version="1.0" encoding="utf-8" standalone="yes"?>
//<entry xml:base="http://myaccount.table.core.windows.net/" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" m:etag="W/&quot;datetime'2008-09-18T23%3A46%3A19.4277424Z'&quot;" xmlns="http://www.w3.org/2005/Atom">
//  <id>http://myaccount.table.core.windows.net/mytable(PartitionKey='mypartitionkey',RowKey='myrowkey1')</id>
//  <title type="text"></title>
//  <updated>2008-09-18T23:46:19.3857256Z</updated>
//  <author>
//    <name />
//  </author>
//  <link rel="edit" title="mytable" href="mytable(PartitionKey='mypartitionkey',RowKey='myrowkey1')" />
//  <category term="myaccount.Tables" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
//  <content type="application/xml">
//    <m:properties>
//      <d:PartitionKey>mypartitionkey</d:PartitionKey>
//      <d:RowKey>myrowkey1</d:RowKey>
//      <d:Timestamp m:type="Edm.DateTime">2008-09-18T23:46:19.4277424Z</d:Timestamp>
//      <d:Address>Mountain View</d:Address>
//      <d:Age m:type="Edm.Int32">23</d:Age>
//      <d:AmountDue m:type="Edm.Double">200.23</d:AmountDue>
//      <d:CustomerCode m:type="Edm.Guid">c9da6455-213d-42c9-9a79-3e9149a57833</d:CustomerCode>
//      <d:CustomerSince m:type="Edm.DateTime">2008-07-10T00:00:00</d:CustomerSince>
//      <d:IsActive m:type="Edm.Boolean">true</d:IsActive>
//      <d:NumOfOrders m:type="Edm.Int64">255</d:NumOfOrders>
//    </m:properties>
//  </content>
//</entry>


//            string body = string.Format(@"<?xml version=""1.0"" encoding=""utf-8"" standalone=""yes""?>
//<entry xmlns:d=""http://schemas.microsoft.com/ado/2007/08/dataservices"" xmlns:m=""http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"" xmlns=""http://www.w3.org/2005/Atom"">
//  <content type=""application/xml"">
//    <m:properties>
//      <d:PartitionKey>{0}</d:PartitionKey>
//      <d:RowKey>{1}</d:RowKey>
//      <d:Address>Mountain View</d:Address>
//      <d:Age m:type=""Edm.Int32"">23</d:Age>
//      <d:AmountDue m:type=""Edm.Double"">200.23</d:AmountDue>
//      <d:BinaryData m:type=""Edm.Binary"" m:null=""true"" />
//      <d:CustomerCode m:type=""Edm.Guid"">c9da6455-213d-42c9-9a79-3e9149a57833</d:CustomerCode>
//      <d:CustomerSince m:type=""Edm.DateTime"">2008-07-10T00:00:00</d:CustomerSince>
//      <d:IsActive m:type=""Edm.Boolean"">true</d:IsActive>
//      <d:NumOfOrders m:type=""Edm.Int64"">255</d:NumOfOrders>
//    </m:properties>
//  </content>
//</entry>", partitionKey, rowKey);


            Stream inputStream = GetXml(AllProperties(partitionKey,rowKey,properties));

            NameValuePairs headers = new NameValuePairs();
            headers[HttpHeader.ContentType] = MediaType.ApplicationAtomXml;

            return new BatchableRequest(HttpMethod.Post, tableName, null, headers, inputStream);
        }







       


        public ClientResponse UpdateEntity(string tableName, string partitionKey, string rowKey, params AzureProperty[] properties)
        {
            return this.Execute(this.UpdateEntityRequest(tableName, partitionKey, rowKey, properties));
        }
        public BatchableRequest UpdateEntityRequest(string tableName, string partitionKey, string rowKey, params AzureProperty[] properties)
        {
            // PUT http://myaccount.table.core.windows.net/myentity(PartitionKey="myPartitionKey", RowKey="myRowKey1")


            string path = string.Format(@"{0}(PartitionKey='{1}',RowKey='{2}')", tableName, partitionKey, rowKey);

          
            NameValuePairs headers = new NameValuePairs();
            headers[HttpHeader.ContentType] = MediaType.ApplicationAtomXml;
            headers[HttpHeader.IfMatch] = "*";

            Stream inputStream = GetXml(AllProperties(partitionKey, rowKey, properties));

            return new BatchableRequest(HttpMethod.Put, path, null, headers, inputStream);

        }



        public ClientResponse MergeEntity(string tableName, string partitionKey, string rowKey, params AzureProperty[] properties)
        {
            return this.Execute(this.MergeEntityRequest(tableName, partitionKey, rowKey, properties));
        }
        public BatchableRequest MergeEntityRequest(string tableName, string partitionKey, string rowKey, params AzureProperty[] properties)
        {
            // MERGE http://myaccount.table.core.windows.net/myentity(PartitionKey="myPartitionKey", RowKey="myRowKey1")


            string path = string.Format(@"{0}(PartitionKey='{1}',RowKey='{2}')", tableName, partitionKey, rowKey);

            
            NameValuePairs headers = new NameValuePairs();
            headers[HttpHeader.ContentType] = MediaType.ApplicationAtomXml;
            headers[HttpHeader.IfMatch] = "*";

            Stream inputStream = GetXml(AllProperties(partitionKey, rowKey, properties));

            return new BatchableRequest("MERGE", path, null, headers, inputStream);



        }


        public ClientResponse DeleteEntity(string tableName, string partitionKey, string rowKey)
        {
            return this.Execute(this.DeleteEntityRequest(tableName, partitionKey, rowKey));
        }
        public BatchableRequest DeleteEntityRequest(string tableName, string partitionKey, string rowKey)
        {
            // DELETE http://myaccount.table.core.windows.net/myentity(PartitionKey="myPartitionKey", RowKey="myRowKey1")

            string path = string.Format(@"{0}(PartitionKey='{1}',RowKey='{2}')", tableName, partitionKey, rowKey);

            NameValuePairs headers = new NameValuePairs();
           // headers[HttpHeader.ContentType] = MediaType.ApplicationAtomXml;
            headers[HttpHeader.IfMatch] = "*";

            return new BatchableRequest(HttpMethod.Delete, path, null, headers, null);
        }





















 

        public ClientResponse GetEntity(string tableName, string partitionKey, string rowKey)
        {
            return this.Execute(this.GetEntityRequest(tableName, partitionKey, rowKey));
        }
        public BatchableRequest GetEntityRequest(string tableName, string partitionKey, string rowKey)
        {
           // GET http://myaccount.table.core.windows.net/mytable(PartitionKey='<partition-key>',RowKey='<row-key>')


            string path = string.Format(@"{0}(PartitionKey='{1}',RowKey='{2}')", tableName, partitionKey, rowKey);

            NameValuePairs headers = new NameValuePairs();
           // headers[HttpHeader.ContentType] = MediaType.ApplicationAtomXml;


            return new BatchableRequest(HttpMethod.Get, path, null, headers, null);
        }

        public ClientResponse QueryEntities(string tableName, string queryExpression, int? limit)
        {
            return QueryEntities(tableName, queryExpression, limit, null, null);
        }
        public BatchableRequest QueryEntitiesRequest(string tableName, string queryExpression, int? limit)
        {
            return QueryEntitiesRequest(tableName, queryExpression, limit, null, null);
        }
        public ClientResponse QueryEntities(string tableName, string queryExpression, int? limit, string nextPartitionKey, string nextRowKey)
        {
            return this.Execute(this.QueryEntitiesRequest(tableName, queryExpression, limit, nextPartitionKey, nextRowKey));
        }
        public BatchableRequest QueryEntitiesRequest(string tableName, string queryExpression, int? limit, string nextPartitionKey, string nextRowKey)
        {
            // GET http://myaccount.table.core.windows.net/mytable()?$filter=<query-expression>


            string path = string.Format(@"{0}()", tableName);

            NameValuePairs headers = new NameValuePairs();
            //headers[HttpHeader.ContentType] = MediaType.ApplicationAtomXml;

            NameValuePairs pathParameters = new NameValuePairs();
            pathParameters["$filter"] = queryExpression;
            if (limit.HasValue)
                pathParameters["$top"] = limit.Value.ToString();
            if (nextPartitionKey != null)
                pathParameters["NextPartitionKey"] = nextPartitionKey;
            if (nextRowKey != null)
                pathParameters["NextRowKey"] = nextRowKey;

            return new BatchableRequest(HttpMethod.Get, path,pathParameters, headers,null);

        }







        public ClientResponse Execute(BatchableRequest request)
        {
            return DoAzureRequest(request.Method, request.Path, null, request.PathParameters, request.Headers, true, request.InputStream, null);
        }

        public ClientResponse ExecuteBatch(BatchableRequest call)
        {
            return ExecuteBatch(Fn.ToEnumerable(call));
        }
        public ClientResponse ExecuteBatch(IEnumerable<BatchableRequest> requests)
        {
            List<BatchableRequest> requestList = Fn.ToList(requests);

            if (requestList.Count == 0)
                throw CodePlexException.Format("nothing to do!");

            bool isGet = Fn.Any(requestList, delegate(BatchableRequest item)
            {
                return item.Method == HttpMethod.Get;
            });

            if (isGet && requestList.Count != 1)
                throw CodePlexException.Format("a get request must be the only request in the batch");



            string batchBoundary = "batch" + Guid.NewGuid().ToString();
            string changesetBoundary = "changeset" + Guid.NewGuid().ToString();

            NameValuePairs headers = new NameValuePairs();
            headers[HttpHeader.ContentType] = string.Format("{0}; boundary={1}", MediaType.MultipartMixed, batchBoundary);

            MemoryStream mem = new MemoryStream();
            StreamWriter writer = new StreamWriter(mem);
            writer.WriteLine("--" + batchBoundary);

            if (!isGet)
            {
                writer.WriteLine("{0}: {1}; boundary={2}", HttpHeader.ContentType, MediaType.MultipartMixed, changesetBoundary);
                writer.WriteLine();
            }


            int id = 1;
            foreach (BatchableRequest call in requestList)
            {
                if (!isGet)
                    writer.WriteLine("--" + changesetBoundary);
                writer.WriteLine("{0}: {1}", HttpHeader.ContentType, MediaType.ApplicationHttp);
                writer.WriteLine("Content-Transfer-Encoding: binary");
                writer.WriteLine();


                writer.WriteLine(call.Method.ToUpper() + " " + this.RootUri + Utils.EncodeUrlPath(call.Path, call.PathParameters) + " HTTP/1.1");
                writer.WriteLine("Content-ID: " + id++);
                foreach (string key in call.Headers.Keys)
                {
                    writer.WriteLine(key + ": " + call.Headers[key]);
                }

                writer.WriteLine();
                writer.Flush();

                if (call.InputStream != null)
                    Utils.BufferedTransfer(call.InputStream, writer.BaseStream);


            }

            writer.WriteLine();
            if (!isGet)
                writer.WriteLine("--{0}--", changesetBoundary);
            writer.WriteLine("--{0}--", batchBoundary);
            writer.Flush();

            mem.Position = 0;


            return this.DoAzureRequest(HttpMethod.Post, "$batch", null, null, headers, true, mem, null);
        }

































        private AzureProperty[] AllProperties(string partitionKey, string rowKey, params AzureProperty[] properties)
        {
            AzureProperty[] allProperties = new AzureProperty[properties.Length + 2];
            allProperties[0] = AzureProperty.String("PartitionKey", partitionKey);
            allProperties[1] = AzureProperty.String("RowKey", rowKey);
            Array.Copy(properties, 0, allProperties, 2, properties.Length);
            return allProperties;

        }


        protected override string SignRequestGetDate(ClientRequest request)
        {
            return request.Headers[AzureConstants.Headers.MicrosoftDate];
        }

        protected override IEnumerable<string> SignRequestAddCanonicalizedHeaders(ClientRequest request)
        {
            yield break;
        }



        private static readonly Dictionary<AzurePropertyType, string> EdmTypeNames = new Dictionary<AzurePropertyType, string>();

        static AzureTableClient()
        {
            EdmTypeNames[AzurePropertyType.EdmBinary] = "Edm.Binary";
            EdmTypeNames[AzurePropertyType.EdmBoolean] = "Edm.Boolean";
            EdmTypeNames[AzurePropertyType.EdmDateTime] = "Edm.DateTime";
            EdmTypeNames[AzurePropertyType.EdmDouble] = "Edm.Double";
            EdmTypeNames[AzurePropertyType.EdmGuid] = "Edm.Guid";
            EdmTypeNames[AzurePropertyType.EdmInt32] = "Edm.Int32";
            EdmTypeNames[AzurePropertyType.EdmInt64] = "Edm.Int64";
        }

             



        private MemoryStream GetXml(params AzureProperty[] properties)
        {
            MemoryStream mem = new MemoryStream();
            XmlWriter w = new XmlTextWriter(mem, Encoding.UTF8);
            w.WriteStartDocument(true);

            w.WriteStartElement("entry");
            w.WriteAttributeString("xmlns:d", "http://schemas.microsoft.com/ado/2007/08/dataservices");
            w.WriteAttributeString("xmlns:m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata");
            w.WriteAttributeString("xmlns", "http://www.w3.org/2005/Atom");

            w.WriteStartElement("content");
            w.WriteAttributeString("type", MediaType.ApplicationXml);

            w.WriteStartElement("m:properties");

            foreach (AzureProperty prop in properties)
            {

                w.WriteStartElement("d:" + prop.Name);
                
                if (prop.Type != AzurePropertyType.EdmString)
                {
                    w.WriteAttributeString("m:type", EdmTypeNames[prop.Type]);
                }

                if (prop.Value == null)
                {
                    w.WriteAttributeString("m:null", "true");
                }
                else if (prop.Type == AzurePropertyType.EdmGuid)
                {
                    w.WriteValue(((Guid)prop.Value).ToString());
                }
                else
                {
                    w.WriteValue(prop.Value);
                }
                w.WriteEndElement();
            }

            w.WriteEndDocument();

            w.Flush();
            mem.Position = 0;

            return mem;
        }

       
    }
}
